Starting with SQL Server 2005, you can use the APPLY operator in a Transact-SQL query to join a table to a table-valued function so the function is evoked for each row returned from the table. For example, you might create a query that returns a list of employees from the Employee table. For each employee, you also want to return a list of the individual job positions that person has held in the company.
You can create a function that retrieves the employees’ positions and then evoke that function for each row returned from the Employee table. Your result set will then include a row for each position that an employee has held. For instance, if John has worked as a salesman and a regional supervisor, the result set will include two rows for John, one for each position. Any data returned from the Employee table will be repeated for each row, but the data returned by the function will be specific to each row in the function’s results.
The APPLY operator can take one of two forms: CROSS APPLY or OUTER APPLY. The CROSS APPLY operator returns rows from the primary (outer) table only if the table-value function produces a result set. That means, in the example above, an employee would be included in the returned data only if that employee has held a specific position within the company. The OUTER APPLY form, on the other hand, returns all rows from the outer table, even if the function produces no results. So an employee would be listed even if that employee held no specific position.
In this article, I demonstrate how to work with both forms of the APPLY operator. The examples I show you were created on a local instance of SQL Server 2008 and the AdventureWorks2008 sample database. If you want to run these examples against the AdventureWorks database on an instance of either SQL Server 2005 or 2008, you must change references to the BusinessEntityID column to SalesPersonID column, where appropriate. In addition, you’ll find that your results might vary slightly from those shown here because the values between the databases are slightly different, particularly primary key values.
Using the CROSS APPLY Operator
As I mentioned, CROSS APPLY returns only those rows in the outer table for which the table value function returns data. Let’s look at an example to demonstrate how this works. Fist, we’ll create a function that returns the top three sales generated by a salesperson, as those sales appear in the Sales.SalesOrderHeader table in the AdventureWorks2008 database:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
USE AdventureWorks2008 GO IF OBJECT_ID (N'fn_sales', N'IF') IS NOT NULL DROP FUNCTION dbo.fn_sales GO CREATE FUNCTION fn_sales (@SalesPersonID int) RETURNS TABLE AS RETURN ( SELECT TOP 3 SalesPersonID, ROUND(TotalDue, 2) AS SalesAmount FROM Sales.SalesOrderHeader WHERE SalesPersonID = @SalesPersonID ORDER BY TotalDue DESC ) GO |
As you can see, the fn_sales function takes one parameter, @SalesPersonID, which is configured with the int data type. The function returns the three highest sales for the specified salesperson. Note that this is a table-valued function, which means that it returns the entire result set generated by the SELECT statement. For the fn_sales function, the result set includes the SalesPersonID and SalesAmount columns.
After you create your function, you can test it by running a SELECT statement that retrieves data from the function. For example, the following SELECT statement returns the SalesAmount column for salesperson ID 285:
1 |
SELECT SalesAmount FROM fn_sales(285) |
The following table shows the results returned by the statement. As you would expect, three rows have been returned-the three highest sales for this salesperson.
SalesAmount |
85652.33 |
45338.76 |
36317.54 |
After you’ve verified that the function is returning the correct results, you can use the function within a statement that includes the CROSS APPLY operator. In the following example, I retrieve data from the Sales.vSalesPerson view and join it to the fn_sales function:
1 2 3 4 5 6 7 8 9 |
SELECT sp.FirstName + ' ' + sp.LastName AS FullName, fn.SalesAmount FROM Sales.vSalesPerson AS sp CROSS APPLY fn_sales(sp.BusinessEntityID) AS fn ORDER BY sp.LastName, fn.SalesAmount DESC |
As you can see, I use the CROSS APPLY operator in the FROM clause by first specifying the outer table (vSalesPerson), then the CROSS APPLY operator, and finally the fn_sales function. Notice that I pass in the BusinessEntityID value as the function’s parameter. This value is based on the BusinessEntityID value as it appears in the current row that is being returned from the outer table.
NOTE: The BusinessEntityID column in the vSalesPerson view uses the same IDs that are used in the SalesPersonID column in the SalesOrderHeader table. In the original version of the AdventureWorks database, the column name in vSalesPerson is SalesPersonID, just like it is in the SalesOrderHeader table.
Also notice that the columns in the SELECT list reference the source table and function as they would if I were joining two tables. I assign an alias to the table (sp) and one to the function (fn) and then reference the columns accordingly. For example, because the SalesAmount column in the SELECT list is returned by the function, I qualify the column name as fn.SalesAmount.
The following table shows part of the results returned by the SELECT statement above. Notice that each salesperson is listed three times, once for each result returned by the fn_sales function.
FullName |
SalesAmount |
Syed Abbas |
85652.33 |
Syed Abbas |
45338.76 |
Syed Abbas |
36317.54 |
Amy Alberts |
98405.08 |
Amy Alberts |
96243.80 |
Amy Alberts |
95193.67 |
Pamela Ansman-Wolfe |
125254.49 |
Pamela Ansman-Wolfe |
125144.01 |
Pamela Ansman-Wolfe |
118284.78 |
Michael Blythe |
198628.31 |
Michael Blythe |
142942.01 |
Michael Blythe |
139659.67 |
David Campbell |
149897.36 |
David Campbell |
136046.44 |
David Campbell |
125068.34 |
Jillian Carson |
162629.75 |
Jillian Carson |
154912.07 |
Jillian Carson |
144355.88 |
Shu Ito |
247913.91 |
Shu Ito |
227737.72 |
Shu Ito |
189198.62 |
Stephen Jiang |
149861.07 |
Stephen Jiang |
114361.94 |
Stephen Jiang |
112733.70 |
Tete Mensa-Annan |
140734.49 |
Tete Mensa-Annan |
137108.39 |
Tete Mensa-Annan |
115068.64 |
That’s all there is to using the CROSS APPLY operator. Not let’s look at another example that uses the operator with a common table expression (CTE).
Using the CROSS APPLY Operator with a CTE
To demonstrate how you can use the APPLY operator with a CTE, I created the following function, which returns the product model associated with the specified product:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
USE AdventureWorks2008 GO IF OBJECT_ID (N'fn_products', N'IF') IS NOT NULL DROP FUNCTION dbo.fn_products GO CREATE FUNCTION fn_products (@ProductID int) RETURNS TABLE AS RETURN ( SELECT p.Name AS ProductName, pm.Name AS ProductModel FROM Production.Product AS p LEFT OUTER JOIN Production.ProductModel AS pm ON p.ProductModelID = pm.ProductModelID WHERE p.ProductID = @ProductID ) GO |
The function joins the Production.Product and Production.ProductModel tables to return the product name and model name for the specified product ID. I then used the following SELECT statement to verify that the function works as I expect:
1 2 |
SELECT ProductName, ProductModel FROM fn_products(707) |
The statement returns the product name and model for product 707 (shown in the following table). As you can see, the product Sport-100 Helmet, Red is associated with the model Sport-100.
ProductName |
ModelName |
Sport-100 Helmet, Red |
Sport-100 |
You can then use the CROSS APPLY operator to join a CTE to the function, as shown in the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
WITH ProductSales (ProductID, TotalSales) AS ( SELECT ProductID, SUM(LineTotal) FROM Sales.SalesOrderDetail GROUP BY ProductID ) SELECT ps.ProductID, ps.TotalSales, fn.ProductModel FROM ProductSales AS ps CROSS APPLY fn_products(ps.ProductID) AS fn ORDER BY ps.ProductID |
Notice that I first define a CTE named ProductSales. The CTE returns the total sales for each product as they appear in the Sales.SalesOrderDetail table. I then use the CTE in the main SELECT statement, along with the fn_products function. Notice that I use the CROSS APPLY operator in the FROM clause to join the CTE to the function, as I would use the operator to join a table or view to the function. The following table shows part of the results returned by the SELECT statement.
ProductID |
TotalSales |
ProductModel |
707 |
157772.394392 |
Sport-100 |
708 |
160869.517836 |
Sport-100 |
709 |
6060.388200 |
Mountain Bike Socks |
710 |
513.000000 |
Mountain Bike Socks |
711 |
165406.617049 |
Sport-100 |
712 |
51229.445623 |
Cycling Cap |
713 |
21445.710000 |
Long-Sleeve Logo Jersey |
714 |
115249.214976 |
Long-Sleeve Logo Jersey |
715 |
198754.975360 |
Long-Sleeve Logo Jersey |
716 |
95611.197080 |
Long-Sleeve Logo Jersey |
717 |
394255.572400 |
HL Road Frame |
718 |
395182.699300 |
HL Road Frame |
719 |
89872.173600 |
HL Road Frame |
722 |
177635.904000 |
LL Road Frame |
723 |
24844.692200 |
LL Road Frame |
725 |
194692.599104 |
LL Road Frame |
726 |
132125.252200 |
LL Road Frame |
727 |
20104.443400 |
LL Road Frame |
729 |
195933.409400 |
LL Road Frame |
730 |
137213.485128 |
LL Road Frame |
732 |
89224.500000 |
ML Road Frame |
733 |
32120.820000 |
ML Road Frame |
736 |
45164.684600 |
LL Road Frame |
738 |
299595.522966 |
LL Road Frame |
739 |
269874.009600 |
HL Mountain Frame |
741 |
141635.100000 |
HL Mountain Frame |
742 |
499556.572400 |
HL Mountain Frame |
743 |
901590.233600 |
HL Mountain Frame |
744 |
13765.920000 |
HL Mountain Frame |
745 |
106078.560000 |
HL Mountain Frame |
One thing you might notice about the results shown here, compared to the results shown in the preceding example, is that the function returns only one row for each product. Because a product is associated with only one product model in the SalesOrderDetail table, there will never be more than one row per product.
Using the OUTER APPLY Operator
In the examples above, the CROSS APPLY operator returns the rows in the outer table for which the table value function returns data. As it turns out, the functions used in both examples return data for all rows in the outer tables. However, in some cases, the function will not return data for a specific row. If you still want the row from the outer table to be included in the result set, you should use the OUTER APPLY operator.
The OUTER APPLY operator returns all rows from the outer table, whether or not the function returns data for a specific row. You use the OUTER APPLY operator just as you would CROSS APPLY; the difference is in the results. Let’s look at an example that demonstrates how this works.
In the following statement, I create a function that returns data about product inventory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
USE AdventureWorks2008 GO IF OBJECT_ID (N'fn_inventory', N'IF') IS NOT NULL DROP FUNCTION dbo.fn_inventory GO CREATE FUNCTION fn_inventory (@ProductID int) RETURNS TABLE AS RETURN ( SELECT ProductID, LocationID, Quantity FROM Production.ProductInventory WHERE ProductID = @ProductID ) GO |
This statement, just like the CREATE FUNCTION statements used in the preceding examples, creates a table-valued function. The function returns three columns: ProductID, LocationID, and Quantity. I verified the function by using the following SELECT statement:
1 2 |
SELECT LocationID, Quantity FROM fn_inventory(915) |
In this case, I specified the product ID of 915 as the function’s parameter. The following table shows the results returned by the statement.
LocationID |
Quantity |
6 |
161 |
50 |
83 |
60 |
158 |
As you can see, product inventory exists in three locations, with different quantities at each location. Once the function has been verified, you can use it with a CROSS APPLY operator, as shown in the following statement:
1 2 3 4 5 6 7 8 9 |
SELECT p.ProductID, p.Name, fn.Quantity FROM Production.Product AS p CROSS APPLY fn_inventory(p.ProductID) AS fn ORDER BY p.ProductID |
Notice that this statement is similar to the previous examples. I’ve simply used the CROSS APPLY operator to join the Product table to the fn_inventory function. The following table shows part of the data returned by the statement. As the results indicate, there are multiple rows for each product, one for each quantity.
ProductID |
Name |
Quantity |
915 |
ML Touring Seat/Saddle |
161 |
915 |
ML Touring Seat/Saddle |
83 |
915 |
ML Touring Seat/Saddle |
158 |
916 |
HL Touring Seat/Saddle |
425 |
916 |
HL Touring Seat/Saddle |
288 |
916 |
HL Touring Seat/Saddle |
276 |
921 |
Mountain Tire Tube |
286 |
921 |
Mountain Tire Tube |
243 |
922 |
Road Tire Tube |
264 |
922 |
Road Tire Tube |
241 |
923 |
Touring Tire Tube |
262 |
923 |
Touring Tire Tube |
240 |
928 |
LL Mountain Tire |
240 |
928 |
LL Mountain Tire |
369 |
929 |
ML Mountain Tire |
385 |
929 |
ML Mountain Tire |
284 |
930 |
HL Mountain Tire |
267 |
930 |
HL Mountain Tire |
232 |
The results shown in the table are those returned for product IDs 915 through 930. As you would expect, each row includes data returned from both the table and the function. Now let’s look at an example that uses OUTER APPLY, rather than CROSS APPLY:
1 2 3 4 5 6 7 8 9 |
SELECT p.ProductID, p.Name, fn.Quantity FROM Production.Product AS p OUTER APPLY fn_inventory(p.ProductID) AS fn ORDER BY p.ProductID |
This statement is exactly like the previous one, except for the APPLY operator. However, the results are slightly different, as shown in the following table:
ProductID |
Name |
Quantity |
915 |
ML Touring Seat/Saddle |
161 |
915 |
ML Touring Seat/Saddle |
83 |
915 |
ML Touring Seat/Saddle |
158 |
916 |
HL Touring Seat/Saddle |
425 |
916 |
HL Touring Seat/Saddle |
288 |
916 |
HL Touring Seat/Saddle |
276 |
917 |
LL Mountain Frame – Silver, 42 |
NULL |
918 |
LL Mountain Frame – Silver, 44 |
NULL |
919 |
LL Mountain Frame – Silver, 48 |
NULL |
920 |
LL Mountain Frame – Silver, 52 |
NULL |
921 |
Mountain Tire Tube |
286 |
921 |
Mountain Tire Tube |
243 |
922 |
Road Tire Tube |
264 |
922 |
Road Tire Tube |
241 |
923 |
Touring Tire Tube |
262 |
923 |
Touring Tire Tube |
240 |
924 |
LL Mountain Frame – Black, 42 |
NULL |
925 |
LL Mountain Frame – Black, 44 |
NULL |
926 |
LL Mountain Frame – Black, 48 |
NULL |
927 |
LL Mountain Frame – Black, 52 |
NULL |
928 |
LL Mountain Tire |
240 |
928 |
LL Mountain Tire |
369 |
929 |
ML Mountain Tire |
385 |
929 |
ML Mountain Tire |
284 |
930 |
HL Mountain Tire |
267 |
930 |
HL Mountain Tire |
232 |
The result set now includes rows for products 917 through 920 and products 924 through 927, which were not included in the previous results. Notice that the Quantity column shows a NULL value for each of the new rows. By using the OUTER APPLY operator, we’re able to return all rows from the outer table, whether or not the function returns any rows.
As you can see, the APPLY operator can be a useful tool when you want to evoke a table-valued function for each row returned by a table expression (the outer table). You simply use the operator to join the outer table to the function. If you want to include only those rows from the outer table for which the function returns data, use the CROSS APPLY operator. If you want to return all rows from the outer table, regardless of whether or not the function returns data for a row, use the OUTER APPLY operator. For more details about either form of the APPLY operator and to see additional examples, check out the topic “Using APPLY” in SQL Server Books Online.
Load comments